This post dives deep into object-oriented programming (OOP), the true essence of C++!
看到过一个很讽刺的笑话:大多数国内高校学生学到的CPP只是C语言的cin/cout 。这当然只是一句玩笑话,但很深刻地反映出对于CPP精髓面向对象编程 的忽视。这是有理可依的:编程语言的学习本身就和传统的课堂授课模式存在较大的出入,编程重视实践 ,枯燥的语法讲解如同天书一般晦涩难懂,更不用提OOP所涉及的都是比较大规模的项目工程 ,如果只是在课堂上乏味地讲解“什么是析构函数,什么是继承,什么是多态··· ”,很容易将CPP学成死记硬背的无聊学科。
因此,笔者希望通过博客的方式,记录自己OOP in CPP 的学习笔记,并分享给各位小伙伴!
Why OOP?
类和对象
类:
类描述了某一类事物应该具有哪些特征 和行为 。比如我们想描述一个“商品”类别,类就会告诉我们商品的名称、编号、进货价格、售出价格等特征,但是这个“商品”并不是一真正的商品,类只告诉了我们“商品”是什么样的、应该具有的特征。类是对象的抽象表示,它本身不占用内存空间 。
类class与结构体struct形式相似,关键字不同!
class的成员默认是private的
struct的成员默认是public的
1 2 3 4 5 6 7 8 class 类名 { private : protected : public : };
private:只能被类本身的成员函数、友元函数、友元函数的成员函数访问,派生类也无法访问。
protected:除派生类可以进行访问,其余与private相同。
public:可以被程序中任意代码访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 class cgoods { private : string ID; string name; double Purchasingprice; double Sellingprice; int SellCount; static double Profit; protected : public : cgoods (string id, string name, double purchasingprice){ } ~cgoods (){ } void SetPurchasingprice (double purchasingprice) { } void SetSellingprice (double sellingprice) { } void setSellingcount (int sellcount) { } void Sell (double sellingprice,int sellcount) { } static double getProfit () { } void display () { } }
对象:
对象则是根据类创建出来的具体实例。比如我们创建一个叫“手机”的“商品”,他有自己的具体的编号、进货价格、售出价格等,所有的特征都是独一无二的,不会与其他商品相同。每一个对象都是根据类创建的具有其属性和方法的实例,并且具有唯一的身份标识(如内存地址)和自己独特的属性值。 对象是占用内存空间的 ,它的属性值可以在运行时动态地改变 。
定义对象的三种方法:
1 2 3 4 class 类名{ 成员表; }; [class ]可选 类名 对象名列表;
例如:
1 2 class cgoods goods1 ("1001001" ,"元气森林" ) ;cgoods goods ("1001001" ,"元气森林" ) ;
例如:
1 2 3 4 class cgoods { private : public : }goods1 ("1001001" ,"元气森林" );
例如:
1 2 3 4 class { private : public : }goods1 ("1001001" ,"元气森林" );
此方法由于没有类名,所以只能一次性声明多个对象 ,此后再无法声明此类对象!
类的成员访问
1 2 3 对象名.成员名 对象指针名->成员名 (*对象指针名).成员名
1 2 3 对象名.成员函数名(参数表) 对象指针名->成员函数名(参数表) (*对象指针名).成员函数名(参数表)
类的构造函数(Constructor)
类的对象太过复杂,一个对象可能有许许多多的数据成员,这就意味着我们要对许许多多的数据成员进行初始化,实现这一过程并不容易。构造函数的作用就是在对象被创建时利用特定的初始值 构造对象,把对象置于某一个初始状态 。
有与类完全相同的名字
没有类型说明 ,不允许有返回值
可以进行重载 ,即一个类中允许定义多个参数不同的构造函数
可以在声明时的参数表里给予初始值
每个类都必须至少有一个构造函数,如果没有显式的为类提供构造函数,则C++提供一个默认的无参构造函数 ,只负责对象的创建,而不做任何初始化的工作
一旦类定义了构造函数,C++不再提供默认的无参构造函数
程序中不能直接调用构造函数,他是在创建类的对象时自动调用 的
1. 无参构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> using namespace std;class Time {public : Time () { hour = 22 ; minute = 22 ; sec = 22 ; } void set_time () ; void show_time () ; private : int hour; int minute; int sec; };void Time::set_time () { cin >> hour; cin >> minute; cin >> sec; }void Time::show_time () { cout << "时间为: " << hour << ":" << minute << ":" << sec << endl; }int main () { Time t1; t1. set_time (); t1. show_time (); Time t2; t2. show_time (); return 0 ; }
1 2 3 12 30 40时间为: 12:30:40 时间为: 22:22:22
2. 含参构造函数
1 2 构造函数名(类型1 形参1 ,类型2 形参2 ,...) 类名 对象名(实参1 ,实参2 ,...)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <iostream> using namespace std;class Cuboid {public : Cuboid (int , int , int ); int volume () ;private : int height; int width; int length; }; Cuboid::Cuboid (int h, int w, int len) { height = h; width = w; length = len; }int Cuboid::volume () { return (height * width * length); }int main () { Cuboid cuboid1 (15 , 45 , 30 ) ; cout << "cuboid1的体积为: " << cuboid1. volume () << endl; Cuboid cuboid2 (10 , 30 , 22 ) ; cout << "cuboid2的体积为: " << cuboid2. volume () << endl; return 0 ; }
1 2 cuboid1 的体积为: 20250 cuboid2 的体积为: 6600
3. 构造函数重载
定义多个构造函数以便给对象提供不同的初始化方法,这些构造函数具有相同的名字而参数的个数或参数的类型不相同 。可以为一个类声明的构造函数的个数是无限制的 ,只要每个构造函数的形参表是唯一的 ,定义对象时会根据提供的实参决定调用哪一个构造函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <iostream> using namespace std;class Cuboid {public : Cuboid (); Cuboid (int h, int w, int len); int volume () ; private : int height; int width; int length; }; Cuboid::Cuboid () { height = 15 ; width = 15 ; length = 15 ; } Cuboid::Cuboid (int h, int w, int len) { height = h; width = w; length = len; }int Cuboid::volume () { return (height * width * length); }int main () { Cuboid cuboid1; cout << "cuboid1的体积为: " << cuboid1. volume () << endl; Cuboid cuboid2 (20 , 30 , 45 ) ; cout << "cuboid2的体积为: " << cuboid2. volume () << endl; return 0 ; }
1 2 cuboid1 的体积为: 3375 cuboid2 的体积为: 27000
4. 使用默认值的构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include <iostream> using namespace std;class Cuboid {private : int height; int width; int length; public : Cuboid (int h = 15 , int w = 15 , int len = 15 ); int volume () ; }; Cuboid::Cuboid (int h, int w, int len) { height = h; width = w; length = len; }int Cuboid::volume () { return (height * width * length); }int main () { Cuboid cuboid1; cout << "cuboid1的体积为: " << cuboid1. volume () << endl; Cuboid cuboid2 (25 ) ; cout << "cuboid2的体积为: " << cuboid2. volume () << endl; Cuboid cuboid3 (25 , 40 ) ; cout << "cuboid3的体积为: " << cuboid3. volume () << endl; Cuboid cuboid4 (25 , 30 , 40 ) ; cout << "cuboid4的体积为: " << cuboid4. volume () << endl; return 0 ; }
1 2 3 4 cuboid1 的体积为: 3375 cuboid2 的体积为: 5625 cuboid3 的体积为: 15000 cuboid4 的体积为: 30000
5.子对象和构造函数
在定义一个新的类时,将一个已有类作为数据成员 ,这个类对象叫做子对象。我们通过调用子对象成员的构造函数来完成对子对象的初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <iostream> using namespace std;class Rectangle { private : int Width, Length; public : Rectangle (int w, int len) { Width = w; Length = len; } int Area () { return (Width * Length); } };class Cuboid { private : int Height; Rectangle r; public : Cuboid (int w, int len, int h) : r (w, len) { Height = h; } int Volume () { return (Height * r.Area ()); } };int main () { Cuboid c1 (10 , 20 , 100 ) ; cout << "长方体 c1 的体积是: " << c1. Volume () << endl; return 0 ; }
1 2 3 Cuboid (int w, int len, int h) : r (w, len) { Height = h; }
实参10、20通过w和len赋值r(w, len),调用类的成员Rectangle(int w, int len)构造函数完成初始化,实参100通过Height = h完成对Cuboid的初始化。
6.拷贝构造函数
设计拷贝构造函数实现类中的一个对象给另一个对象的每个非静态数据成员赋值 。(用已经初始化的对象去初始化一个新定义的对象 )
拷贝构造函数的函数名必须与类名一致 ,函数的形式参数是本类型的一个引用变量 ,必须为引用 !
自定义拷贝构造函数 ,能够实现有选择 的复制原对象中的数据(实现对部分数据的修改)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <iostream> using namespace std;class Sample { private : int nTest; public : Sample (int ly) { nTest = ly; } Sample (Sample &tS) { cout << "拷贝构造函数被调用" << endl; nTest = tS.nTest + 8 ; } int readtest () { return nTest; } void settest (int ly) { nTest = ly; } };int main () { Sample S1 (100 ) ; Sample S2 (S1) ; cout << S2. readtest () << endl; return 0 ; }
类的析构函数(Destructor)
相当于创建对象时用new申请了一片内存空间,应在退出前在析构函数中用delete释放 。析构函数是与构造函数作用相反的函数,当对象生命周期结束时,自动执行析构函数。
有与类完全相同的名字,只是在函数名前面加一个位取反符“~” ,以区别于构造函数
不带任何参数,没有返回值
一个类最多只能有一个析构函数,无法进行重载
如果用户没有编写析构函数,编译系统会自动的生成一个默认的析构函数
1 2 3 4 5 6 class 类名{ public : ~类名();{ } };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 #include <iostream> #include <string> using namespace std;class Person {private : string name; int age; char gender; string idNumber; public : Person (string, int , char , string); Person (Person&); ~Person (); string getName () ; void showInfo () ; }; Person::Person (string theName, int theAge, char theGender, string theIdNumber) { name = theName; age = theAge; gender = theGender; idNumber = theIdNumber; cout << name << " Constructor called." << endl; } Person::Person (Person& theObject) { name = theObject.name; age = theObject.age; gender = theObject.gender; idNumber = theObject.idNumber; cout << "Copy Constructor called." << endl; } Person::~Person () { cout << name << " Destructor called." << endl; }string Person::getName () { return name; }void Person::showInfo () { cout << "name: " << name << endl; cout << "age: " << age << endl; cout << "gender: " << gender << endl; cout << "id number: " << idNumber << endl; }int main () { Person p1 ("张三" , 12 , 'm' , "12345200006061111" ) ; Person p2 ("李四" , 31 , 'f' , "12345198111091234" ) ; p1. showInfo (); p2. showInfo (); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 张三 Constructor called .李四 Constructor called . name : 张三 age: 12 gender: m id number: 12345200006061111 name : 李四 age: 31 gender: f id number: 12345198111091234 李四 Destructor called .张三 Destructor called .
注意观察输出:构造函数和析构函数调用的顺序!
构造函数和析构函数调用顺序
一般而言,调用构造函数的次序与调用析构函数的次序相反 ,与栈类似:先调用构造函数的对象,最后调用析构函数 。
特殊情况:
全局定义 对象(函数体外定义的对象):程序开始之前调用构造函数,程序结束或调用exit()函数时调用析构函数。局部定义 的对象(函数体内定义的对象):程序执行到定义对象的地方时调用构造函数,函数结束时调用析构函数。static定义 的对象:在首次到达对象定义位置时调用构造函数,程序结束时调用析构函数。new动态生成 的对象:产生对象时调用构造函数,用delete释放对象时,才调用析构函数。若不使用delete运算符来撤销动态生成的对象,则析构函数不会被调用。
对象的动态建立和释放
new运算符建立对象:先为类的对象分配内存空间 ,然后自动调用构造函数初始化 对象的数据成员,最后将变量的起始地址返还给指针变量。
delete运算符释放对象:只有在delete运算符释放对象时,才会调用析构函数将对象销毁 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include <iostream> using namespace std;class Complex {public : Complex (double r, double i) { real = r; imag = i; cout << "构造函数被调用" << endl; } ~Complex () { cout << "析构函数被调用" << endl; } void display () { cout << "(" << real << "," << imag << ")" << endl; }private : double real; double imag; };int main () { Complex* pc1 = new Complex (3 , 4 ); pc1->display (); delete pc1; cout << "程序结束" << endl; return 0 ; }
1 2 3 4 构造函数被调用 (3 ,4 ) 析构函数被调用 程序结束
静态成员
声明为static的类成员称为静态成员,可以被类的所有对象共享 。
静态数据成员:描述这一类对象所共有的数据,所有对象公用这一部分存储空间。
静态数据函数
eg:Profit和 static double getProfit() ,总利润是出售所有商品获得的,并不隶属于哪一个商品对象。
为什么不使用全局变量?
违背了OOP封装性的精神 ,任何地方都可以对全局变量进行访问,破坏了信息隐藏原则过多使用全局变量会产生重名冲突 能够明确归属 ,直接表明它是类的一部分,便于进行初始化
静态数据成员
在类的定义中的数据成员声明前加上关键字static,表示该成员是静态数据成员。由于静态数据成员由类的所有对象共享 ,所以静态成员的存储空间不会随着对象的产生而分配 ,也不会随着对象的消失而释放 ,因此静态数据成员不能在类体内进行初始化,而只能在类体内进行声明 ,在类体外进行初始化 。
注意 :
不需要加static关键字
需要通过作用域运算符::限定修饰
1 2 3 4 5 6 7 8 class cgoods { private : ...... static double Profit; public : ...... }double cgoods::Profit=0 ;
类外的定义是必要的,若没有明确赋初值,则编译系统会自动赋初值为0。
静态成员函数
与类的数据成员相同,在成员函数前加上static可以创建一个静态成员函数。静态函数没有this指针,通常他只访问属于全体对象的成员————即静态成员。
1 2 3 double cgoods::getProfit () { return Profit; }
非静态成员函数可以任意地访问静态成员函数和静态数据成员 静态成员函数不能直接访问非静态成员函数和非静态数据成员
静态成员的访问
用类的对象访问 || 直接用作用域运算符“::”通过类名访问
静态成员函数的访问与静态成员数据的访问的形式相同,不做过多阐释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #include <iostream> #include <string> using namespace std;class CStudent {private : string SName; float Score; static int studentTotal; static float SumScore; public : CStudent (string name, float sc); static float average () ; void Print () ; ~CStudent (); };int CStudent::studentTotal = 0 ;float CStudent::SumScore = 0 ; CStudent::CStudent (string name, float sc) { SName = name; Score = sc; studentTotal++; SumScore += sc; cout << SName << " Constructor called." << endl; }void CStudent::Print () { cout << SName << ": " << Score << endl; }float CStudent::average () { if (studentTotal == 0 ) return 0 ; return (SumScore / studentTotal); } CStudent::~CStudent () { studentTotal--; SumScore -= Score; cout << SName << " Destructor called." << endl; }int main () { CStudent stud1 ("Zhang San" , 90 ) ; CStudent stud2 ("Li Si" , 80 ) ; stud1. Print (); stud2. Print (); cout << "平均分为: " << CStudent::average () << endl; return 0 ; }
1 2 3 4 5 6 7 Zhang San Constructor called .Li Si Constructor called .Zhang San : 90 Li Si: 80 平均分为: 85 Li Si Destructor called .Zhang San Destructor called .
this指针
用途:当成员函数的参数名与成员变量名相同 的时候,可以使用this来明确地引用成员变量
1 2 3 4 5 Ponit& setPoint (int x,int y) { this ->x=x; (*this ).y=y+8 ; return *this ; }
this指针是一个指向对象的指针 this指针是一个隐含于成员函数中的对象指针 this指针是一个指向正在调用成员函数的对象的指针 类的静态成员函数没有this指针
常对象
常对象用const进行修饰,常对象必须进行初始化,且不能被更新,常对象的声明如下(两种声明完全相同,没有任何区别):
1 2 const 类名 对象名[(实参列表)]; 类名 const 对象名[(实参列表)];
1 2 const Point P1 (1 ,1 ) ;Point const P2 (2 ,2 ) ;
以上定义了两个常对象P1、P2。在任何场合,对象P1、P2中的成员值不能进行修改。
常对象不能调用非const成员函数:
1 2 3 int Area () const { return x*y; }
如果一定要修改常对象中的数据成员,可将需要修改的数据成员声明为mutable,这样就可以用声明为const的成员函数来修改它的值了!
友元类
若我们想要一个不属于某个类的函数存取该类中的数据:
将类中的数据成员均设置为public
在类内部声明 这个函数为友元(friend),则这个函数可以访问该类的私有成员
第一点有违OOP封装性的精神,显然第二种更好!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <iostream> #include <string> using namespace std;class Person {private : string name; int age; char gender; string idNumber; public : Person (string, int , char , string); Person (Person&); ~Person () {} string getName () ; friend void showInfo (Person& p) ; }; Person::Person (string theName, int theAge, char theGender, string theIdNumber) { name = theName; age = theAge; gender = theGender; idNumber = theIdNumber; } Person::Person (Person& theObject) { name = theObject.name; age = theObject.age; gender = theObject.gender; idNumber = theObject.idNumber; }string Person::getName () { return name; }void showInfo (Person& p) { cout << "name: " << p.name << endl; cout << "age: " << p.age << endl; cout << "gender: " << p.gender << endl; cout << "id number: " << p.idNumber << endl; }int main () { Person p1 ("张三" , 12 , 'm' , "12345200006061111" ) ; showInfo (p1); return 0 ; }
1 2 3 4 name: 张三 age: 12 gender: m id number: 12345200006061111
注意:
友元函数是类外函数,友元函数不能直接访问类中的私有和保护成员,而需要通过对象参数进行访问
这个案例显然并没有那么好,我们可以将showInfo函数设计为类内一个普通的成员函数,这没有显示出友元函数的必要性,仅仅是对友元函数用法的一个初步介绍!
友元函数是另一个类的成员函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <iostream> using namespace std;class Rectangle ; class Cuboid {private : int Height;public : Cuboid (int h) : Height (h) {} int Volume (Rectangle& r) ; };class Rectangle {private : int Width, Length;public : Rectangle (int w, int len) : Width (w), Length (len) {} friend int Cuboid::Volume (Rectangle& r) ; };int Cuboid::Volume (Rectangle& r) { return r.Length * r.Width * Height; }int main () { Rectangle R (6 , 8 ) ; Cuboid C (20 ) ; cout << "长方体的体积为:" << C.Volume (R) << endl; return 0 ; }
这里将类Cuboid的成员函数Volume()声明为类Rectangle的友元函数,这样在Volume()中就可以使用Rectangle中的私有数据成员Width、Length。
注释:
程序第三行对Rectangle的提前声明引用 ,只包含类名,不包含类体。提前声明的原因是:在类Cuboid中调用Volume()函数时,需要使用类Rectangle中的数据成员Length和Width,但是类Rectangle还没有定义。那如果将Rectangle的定义提到前面呢?同样是不可以的,因为在类Rectangle中又包含了Cuboid的成员!但是不能因为提前声明,而去定义一个对象!
例如:
1 2 3 class Rectangle ; Rectangle r1;class Rectangle {...};
友元类
将一个类声明为另一个类的友元:
1 2 3 4 5 class A { ... friend class B ; ... };
此时,类B中的所有成员函数都是当前类A的友元函数,因此类B中的所有成员函数 都可以访问当前类A的private成员或protected成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> using namespace std;class Rectangle ; class Cuboid {private : int Height;public : Cuboid (int h) { Height = h; } int Volume (Rectangle& r) ; };class Rectangle {private : int Width, Length;public : Rectangle (int w, int len) { Width = w; Length = len; } friend class Cuboid ; };int Cuboid::Volume (Rectangle& r) { return r.Length * r.Width * Height; }int main () { Rectangle r (6 , 8 ) ; Cuboid C (20 ) ; cout << "长方体的体积为:" << C.Volume (r) << endl; return 0 ; }
友元关系的限制:
友元关系不具有传递性 ,“附庸的附庸不是我的附庸”,比如类A是类B的友元类,类B是类C的友元类,类C不是类A的友元类。 友元关系不具有交换性 ,比如类A是类B的友元类,类B不一定是类A的友元 友元关系是不能继承的 ,比如类A是类B的友元类,类C继承类B,类C不是类A的友元类
封面来源:Fundamental Concepts of Object Oriented Programming
References
以上大部分代码均取自《C++程序设计基础教程》